/*
 * Copyright (C) 2010 TopCoder Inc., All Rights Reserved.
 */
package com.hercules.xre.protocol.testing.handlers.commandretriever;

import com.hercules.xre.protocol.testing.handlers.CommandRetriever;
import com.hercules.xre.protocol.testing.handlers.CommandRetrieverException;
import com.hercules.xre.protocol.testing.handlers.EventType;
import com.hercules.xre.protocol.testing.handlers.Helper;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Represents {@link FileCommandRetriever} class. This is default implementation of {@link CommandRetriever} interface.
 * It reads list of commands from a file. It caches the result.
 * <p>
 * <b>Thread safety:</b> This class is effectively thread safe (setFilename is expected to be called once in beginning)
 * since the commands are initialized only once.
 *
 * @author bramandia, 7realm
 * @version 1.0
 */
public class FileCommandRetriever implements CommandRetriever {
    /** Represents command ending delimiter. */
    private static final String COMMAND_DELIM = "########## COMMAND ##########";
    /** Represents end of file separator. */
    private static final String EOL = System.getProperty("line.separator");
    /** Represents corresponding even types for TYPE_DELIMS. */
    private static final List<EventType> EVENT_TYPES =
        Arrays.asList(EventType.CONNECTION_CREATED, EventType.CONNECTION_EVENT, EventType.CONNECTION_SHUTDOWN);
    /** Represents type delimiters, used in file. The last delimiter is null, that means end of file. */
    private static final List<String> TYPE_DELIMS =
        Arrays.asList("########## CONNECTION_CREATED ##########", "########## CONNECTION_EVENT ##########",
            "########## CONNECTION_SHUTDOWN ##########", null);

    /**
     * Represents the map between the event type enumeration to the list of commands. The key is EventType enumeration.
     * The value is a non-null (can be empty) list of String. The list must not have null/empty element. Will not
     * change. It is used in retrieveCommands. Initialized in retrieveCommands.
     */
    private Map<EventType, List<String>> commands;
    /**
     * Represents the input filename where the commands are stored. Initialized to null. Has getter/setter. It is used
     * in retrieveCommands. Must not be null/empty.
     */
    private String filename;

    /**
     * Empty default constructor.
     */
    public FileCommandRetriever() {
        // empty
    }

    /**
     * Gets the input filename where the commands are stored.
     *
     * @return the input filename where the commands are stored
     */
    public String getFilename() {
        return filename;
    }

    /**
     * Sets for the input filename where the commands are stored.
     *
     * @param filename the new value of input filename where the commands are stored
     * @throws IllegalArgumentException if filename is null/empty
     */
    public void setFilename(String filename) {
        // check argument
        Helper.checkString(filename, "filename");
        this.filename = filename;

        // set commands to null in order to reload them
        commands = null;
    }

    /**
     * Retrieves commands for the given event type.
     *
     * @param eventType the event type, must not be null
     * @return the list of command strings for the given event type, if there is no such command, empty list will be
     * returned
     * @throws IllegalArgumentException if eventType is null
     * @throws IllegalStateException if filename is not set
     * @throws CommandRetrieverException if there is any error during retrieving commands
     */
    public List<String> retrieveCommands(EventType eventType) throws CommandRetrieverException {
        // check argument
        Helper.checkNull(eventType, "eventType");

        // check state
        Helper.checkState(filename != null, "Filename should be set before calling retrieveCommands method.");

        // check if commands need to be loaded
        if (commands == null) {
            Map<EventType, List<String>> newCommands = new HashMap<EventType, List<String>>();
            try {
                LineNumberReader reader = new LineNumberReader(new FileReader(filename));
                try {
                    // load commands from file
                    String line = readNextLine(reader, true, filename + "'. File is empty.");
                    if (!TYPE_DELIMS.get(0).equals(line)) {
                        throw new CommandRetrieverException("Expected type delimiter '" + TYPE_DELIMS.get(0)
                            + "' at beginning of the file '" + filename + "'.");
                    }
                    for (int i = 0; i < TYPE_DELIMS.size() - 1; i++) {
                        readEventType(filename, newCommands, reader, i + 1);
                    }
                } finally {
                    reader.close();
                }
            } catch (IOException e) {
                throw new CommandRetrieverException("I/O error occurred while retrieving commands from file '"
                    + filename + "'.", e);
            }
            commands = newCommands;
        }

        // get result from commands
        List<String> result = commands.get(eventType);
        return result == null ? new ArrayList<String>() : result;
    }

    /**
     * Read all commands from one block.
     *
     * @param filename the filename, used for exception messages
     * @param newCommands the commands map that will be populated
     * @param reader the reader of the input file
     * @param endDelimIndex the index of ending delimiter
     * @throws CommandRetrieverException if any error occurred during reading commands from file
     * @throws IOException if an I/O error occurred
     */
    private static void readEventType(String filename, Map<EventType, List<String>> newCommands,
        LineNumberReader reader, int endDelimIndex) throws CommandRetrieverException, IOException {
        List<String> commands = new ArrayList<String>();
        String nextTypeDelim = TYPE_DELIMS.get(endDelimIndex);
        while (true) {
            String line =
                readNextLine(reader, nextTypeDelim != null, filename + "', expected next type delimiter '"
                    + nextTypeDelim + "'.");

            // check if line is not next type delimiter
            if (line == null || line.equals(nextTypeDelim)) {
                newCommands.put(EVENT_TYPES.get(endDelimIndex - 1), commands);
                break;
            }

            // if we get command delimiter here, then command is empty
            if (COMMAND_DELIM.equals(line)) {
                throw new CommandRetrieverException("Unexpected command delimiter at '" + filename + ":"
                    + reader.getLineNumber() + "', possibly empty command is present.");
            }

            // read command
            StringBuilder command = new StringBuilder();
            while (true) {
                if (COMMAND_DELIM.equals(line)) {
                    commands.add(command.toString());
                    break;
                }
                if (TYPE_DELIMS.contains(line)) {
                    throw new CommandRetrieverException("Unexpected type delimiter at '" + filename + ":"
                        + reader.getLineNumber() + "', expected ending command delimiter.");
                }
                command.append(line).append(EOL);
                line = readNextLine(reader, true, filename + "', expected ending command delimiter.");
            }
        }
    }

    /**
     * Read next line from file, empty lines are skipped.
     *
     * @param reader the reader of input file
     * @param errorMessage the error message if end of file was not expected
     * @param allowEndOfFile indicates if we should expect end of file
     * @return the next not empty line, or null if reached the end of file
     * @throws IOException if an I/O error occurred
     * @throws CommandRetrieverException if reached end of file, and this was not expected
     */
    private static String readNextLine(BufferedReader reader, boolean allowEndOfFile, String errorMessage)
        throws IOException, CommandRetrieverException {
        while (true) {
            String line = reader.readLine();
            if (line == null) {
                if (allowEndOfFile) {
                    throw new CommandRetrieverException("Unexpected end of file '" + errorMessage);
                }
                return null;
            }
            if (line.trim().length() != 0) {
                return line;
            }
        }
    }
}
